<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">

    <title>3D Stripe Gradient — Alien.js</title>

    <link rel="preconnect" href="https://fonts.gstatic.com">
    <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto+Mono">
    <link rel="stylesheet" href="../assets/css/style.css">

    <style>
        :root {
            --gradient-color-1: white;
            --gradient-color-2: gainsboro;
            --gradient-color-3: lightgray;
            --gradient-color-4: silver;
        }

        @media (prefers-color-scheme: dark) {
            :root {
                --gradient-color-1: black;
                --gradient-color-2: dimgray;
                --gradient-color-3: gray;
                --gradient-color-4: darkgray;
            }
        }
    </style>

    <script type="module">
        import { Color, ColorManagement, GLSL3, Group, LinearSRGBColorSpace, Mesh, OrthographicCamera, PanelItem, PlaneGeometry, RawShaderMaterial, Scene, UI, Vector2, WebGLRenderer, WireframeOptions, getKeyByValue, ticker } from '../../build/alien.three.js';

        // Based on https://www.youtube.com/watch?v=LW9d2cqIHb4 by akella

        import simplex3d from '../../src/shaders/modules/noise/simplex3d.glsl.js';

        class GradientMaterial extends RawShaderMaterial {
            constructor() {
                super({
                    glslVersion: GLSL3,
                    uniforms: {
                        uColor: { value: [new Color(), new Color(), new Color(), new Color()] },
                        uTime: { value: 0 }
                    },
                    vertexShader: /* glsl */ `
                        in vec3 position;
                        in vec3 normal;
                        in vec2 uv;

                        uniform mat4 modelViewMatrix;
                        uniform mat4 projectionMatrix;

                        uniform vec3 uColor[4];
                        uniform float uTime;

                        out vec3 vColor;

                        ${simplex3d}

                        void main() {
                            vec2 noiseCoord = uv * vec2(3, 4);

                            float tilt = -0.05 * uv.y;

                            float offset = 0.5 * mix(-0.25, 0.25, uv.y);

                            float noise = snoise(vec3(noiseCoord.x + uTime * 0.1, noiseCoord.y, uTime * 0.2));

                            noise = max(0.0, noise);

                            vec3 pos = vec3(position.x, position.y + noise * 0.1 + tilt + offset, position.z);

                            vColor = uColor[0];

                            for (int i = 1; i < 4; i++) {
                                float noiseFlow = 0.1;
                                float noiseSpeed = 0.2;

                                float noiseSeed = 1.0 + float(i) * 10.0;
                                vec2 noiseFreq = vec2(1.0, 1.4) * 0.4;

                                float noiseFloor = 0.0;
                                float noiseCeil = 1.0;

                                float noise = smoothstep(noiseFloor, noiseCeil,
                                    snoise(
                                        vec3(
                                            noiseCoord.x * noiseFreq.x + uTime * noiseFlow,
                                            noiseCoord.y * noiseFreq.y,
                                            uTime * noiseSpeed + noiseSeed
                                        )
                                    )
                                );

                                vColor = mix(vColor, uColor[i], noise);
                            }

                            gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
                        }
                    `,
                    fragmentShader: /* glsl */ `
                        precision highp float;

                        in vec3 vColor;

                        out vec4 FragColor;

                        void main() {
                            FragColor = vec4(vColor, 1.0);
                        }
                    `,
                    // wireframe: true,
                    transparent: true,
                    depthTest: false,
                    depthWrite: false
                });
            }
        }

        class SceneView extends Group {
            constructor() {
                super();

                this.initMesh();
            }

            initMesh() {
                const geometry = new PlaneGeometry(1, 1, 96, 96);

                const material = new GradientMaterial();

                this.mesh = new Mesh(geometry, material);
                this.mesh.frustumCulled = false;
                this.add(this.mesh);
            }

            // Public methods

            theme = () => {
                const rootStyle = getComputedStyle(document.querySelector(':root'));

                this.mesh.material.uniforms.uColor.value[0].set(rootStyle.getPropertyValue('--gradient-color-1').trim());
                this.mesh.material.uniforms.uColor.value[1].set(rootStyle.getPropertyValue('--gradient-color-2').trim());
                this.mesh.material.uniforms.uColor.value[2].set(rootStyle.getPropertyValue('--gradient-color-3').trim());
                this.mesh.material.uniforms.uColor.value[3].set(rootStyle.getPropertyValue('--gradient-color-4').trim());
            };

            resize = (width, height) => {
                this.mesh.scale.set(width, height, 1);
            };

            update = time => {
                this.mesh.material.uniforms.uTime.value = time;
            };

            ready = () => Promise.all([
            ]);
        }

        class SceneController {
            static init(view) {
                this.view = view;
            }

            // Public methods

            static theme = () => {
                this.view.theme();
            };

            static resize = (width, height) => {
                this.view.resize(width, height);
            };

            static update = time => {
                this.view.update(time);
            };

            static animateIn = () => {
            };

            static ready = () => this.view.ready();
        }

        class PanelController {
            static init(view, ui) {
                this.view = view;
                this.ui = ui;

                this.initPanel();
            }

            static initPanel() {
                const { mesh } = this.view;

                const items = [
                    {
                        name: 'FPS'
                    },
                    {
                        type: 'divider'
                    },
                    {
                        type: 'color',
                        value: mesh.material.uniforms.uColor.value[0],
                        callback: value => {
                            mesh.material.uniforms.uColor.value[0].copy(value);
                        }
                    },
                    {
                        type: 'color',
                        value: mesh.material.uniforms.uColor.value[1],
                        callback: value => {
                            mesh.material.uniforms.uColor.value[1].copy(value);
                        }
                    },
                    {
                        type: 'color',
                        value: mesh.material.uniforms.uColor.value[2],
                        callback: value => {
                            mesh.material.uniforms.uColor.value[2].copy(value);
                        }
                    },
                    {
                        type: 'color',
                        value: mesh.material.uniforms.uColor.value[3],
                        callback: value => {
                            mesh.material.uniforms.uColor.value[3].copy(value);
                        }
                    },
                    {
                        type: 'divider'
                    },
                    {
                        type: 'list',
                        list: WireframeOptions,
                        value: getKeyByValue(WireframeOptions, mesh.material.wireframe),
                        callback: value => {
                            mesh.material.wireframe = WireframeOptions[value];
                        }
                    }
                ];

                items.forEach(data => {
                    this.ui.addPanel(new PanelItem(data));
                });
            }
        }

        class RenderManager {
            static init(renderer, scene, camera) {
                this.renderer = renderer;
                this.scene = scene;
                this.camera = camera;
            }

            // Public methods

            static resize = (width, height, dpr) => {
                this.renderer.setPixelRatio(dpr);
                this.renderer.setSize(width, height);
            };

            static update = () => {
                this.renderer.render(this.scene, this.camera);
            };
        }

        class WorldController {
            static init() {
                this.initWorld();

                this.addListeners();
            }

            static initWorld() {
                this.renderer = new WebGLRenderer({
                    powerPreference: 'high-performance',
                    antialias: true
                });

                // Output canvas
                this.element = this.renderer.domElement;

                // Disable color management
                ColorManagement.enabled = false;
                this.renderer.outputColorSpace = LinearSRGBColorSpace;

                // 2D scene
                this.scene = new Scene();
                this.camera = new OrthographicCamera(-1, 1, 1, -1, 0, 1);

                // Global uniforms
                this.resolution = { value: new Vector2() };
                this.texelSize = { value: new Vector2() };
                this.aspect = { value: 1 };
                this.time = { value: 0 };
                this.frame = { value: 0 };
            }

            static addListeners() {
                this.renderer.domElement.addEventListener('touchstart', this.onTouchStart);
            }

            // Event handlers

            static onTouchStart = e => {
                e.preventDefault();
            };

            // Public methods

            static resize = (width, height, dpr) => {
                this.camera.left = -width / 2;
                this.camera.right = width / 2;
                this.camera.top = height / 2;
                this.camera.bottom = -height / 2;
                this.camera.updateProjectionMatrix();

                width = Math.round(width * dpr);
                height = Math.round(height * dpr);

                this.resolution.value.set(width, height);
                this.texelSize.value.set(1 / width, 1 / height);
                this.aspect.value = width / height;
            };

            static update = (time, delta, frame) => {
                this.time.value = time;
                this.frame.value = frame;
            };
        }

        class App {
            static async init() {
                this.initWorld();
                this.initViews();
                this.initControllers();

                this.addListeners();
                this.onTheme();
                this.onResize();

                await SceneController.ready();
                SceneController.animateIn();

                this.initPanel();
            }

            static initWorld() {
                WorldController.init();
                document.body.appendChild(WorldController.element);
            }

            static initViews() {
                this.view = new SceneView();
                WorldController.scene.add(this.view);

                this.ui = new UI({ fps: true });
                this.ui.animateIn();
                document.body.appendChild(this.ui.element);
            }

            static initControllers() {
                const { renderer, scene, camera } = WorldController;

                SceneController.init(this.view);
                RenderManager.init(renderer, scene, camera);
            }

            static initPanel() {
                PanelController.init(this.view, this.ui);
            }

            static addListeners() {
                window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', this.onTheme);
                window.addEventListener('resize', this.onResize);
                ticker.add(this.onUpdate);
                ticker.start();
            }

            // Event handlers

            static onTheme = () => {
                SceneController.theme();
            };

            static onResize = () => {
                const width = document.documentElement.clientWidth;
                const height = document.documentElement.clientHeight;
                const dpr = window.devicePixelRatio;

                WorldController.resize(width, height, dpr);
                SceneController.resize(width, height);
                RenderManager.resize(width, height, dpr);
            };

            static onUpdate = (time, delta, frame) => {
                WorldController.update(time, delta, frame);
                SceneController.update(time);
                RenderManager.update(time, delta, frame);
                this.ui.update();
            };
        }

        App.init();
    </script>
</head>
<body>
</body>
</html>
